﻿using System;
using System.Buffers;
using System.Dynamic;
using System.IO;
using System.Net.Http;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;

namespace Common
{
    /// <summary>
    /// Dynamic JSON base class.
    /// Provides static factory methods.
    /// </summary>
    public abstract class DynamicJson : DynamicObject
    {
        protected readonly JsonNode Element;
        protected readonly JsonSerializerOptions Options;

        protected DynamicJson(JsonNode element, JsonSerializerOptions options)
        {
            Element = element;
            Options = options;
        }

        /// <summary>
        /// Return raw JSON
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return Element.GetValueKind() == JsonValueKind.String
                ? Element.ToString()
                : Element.ToJsonString(JsonHelper.JsonSerializerOptions);
        }

        #region create

        /// <summary>
        /// 
        /// </summary>
        /// <param name="element"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        public static dynamic Create(JsonNode element, JsonSerializerOptions options = null)
        {
            switch (element.GetValueKind())
            {
                case JsonValueKind.Undefined:
                case JsonValueKind.Null:
                    return null;
                case JsonValueKind.True:
                case JsonValueKind.False:
                    return element.GetValue<bool>();
                case JsonValueKind.Number:
                    return element.GetValue<double>();
                case JsonValueKind.Array:
                    return new DynamicJsonArray((JsonArray)element, options);
                case JsonValueKind.String:
                    return element.ToString();
                case JsonValueKind.Object:
                default:
                    return new DynamicJsonObject((JsonObject)element, options);
            }
        }
        #endregion

        #region parse
        /// <summary>
        /// Parse text representing a single JSON string value into dynamic JSON.
        /// </summary>
        /// <param name="json">The JSON text to parse</param>
        /// <param name="options">Options to control the behavior during reading.</param>
        /// <returns></returns>
        public static dynamic Parse(string json, JsonSerializerOptions options = null)
        {
            if (json == null)
                throw new ArgumentNullException(nameof(json));
            var doc = JsonNode.Parse(json, JsonHelper.GetNodeOptions(options), JsonHelper.GetDocumentOptions(options));
            return Create(doc, options);
        }

        /// <summary>
        /// Parse a Stream as UTF-8-encoded data representing a single JSON value into dynamic JSON. The stream is read to completion.
        /// </summary>
        /// <param name="json">The JSON data to parse</param>
        /// <param name="options">Options to control the behavior during reading.</param>
        /// <returns></returns>
        public static dynamic Parse(Stream json, JsonSerializerOptions options = null)
        {
            if (json == null)
                throw new ArgumentNullException(nameof(json));
            var doc = JsonNode.Parse(json, JsonHelper.GetNodeOptions(options), JsonHelper.GetDocumentOptions(options));
            return Create(doc, options);
        }

        /// <summary>
        /// Parse a Stream as UTF-8-encoded data representing a single JSON value into dynamic JSON. The stream is read to completion.
        /// </summary>
        /// <param name="json">The JSON data to parse</param>
        /// <param name="options">Options to control the behavior during reading.</param>
        /// <param name="cancellationToken">A token that may be used to cansel the read operation.</param>
        /// <returns></returns>
        public static async Task<dynamic> ParseAsync(
            Stream json,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default)
        {
            if (json == null)
                throw new ArgumentNullException(nameof(json));
            var doc = await JsonNode.ParseAsync(json, JsonHelper.GetNodeOptions(options), JsonHelper.GetDocumentOptions(options), cancellationToken);
            return Create(doc, options);
        }

        #endregion

        #region read file
        /// <summary>
        /// Read a file at the specified path as a dynamic JSON.
        /// </summary>
        /// <param name="file">A relative or absolute path to the file.</param>
        /// <param name="options">Options to control the behavior during reading.</param>
        /// <returns></returns>
        public static dynamic Read(string file, JsonSerializerOptions options = null)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));

            using (var stream = File.OpenRead(file))
            {
                return Parse(stream, options);
            }
        }

        /// <summary>
        /// Read a file at the specified path as a dynamic JSON.
        /// </summary>
        /// <param name="file">A relative or absolute path to the file.</param>
        /// <param name="options">Options to control the behavior during reading.</param>
        /// <param name="cancellationToken">A token that may be used to cansel the read operation.</param>
        /// <returns></returns>
        public static async Task<dynamic> ReadAsync(
            string file,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));

            using (var stream = File.OpenRead(file))
            {
                return await ParseAsync(stream, options, cancellationToken);
            }
        }
        #endregion

        #region get
        /// <summary>
        /// Send a GET request to the specified Uri and return the response body as a dynamic JSON.
        /// </summary>
        /// <param name="uri">The Uri the request is sent to.</param>
        /// <param name="options">Options to control the behavior during reading.</param>
        /// <param name="cancellationToken">A token that may be used to cansel the read operation.</param>
        /// <returns></returns>
        public static async Task<dynamic> GetAsync(
            string uri,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default)
        {
            return await GetAsync(new Uri(uri), options, cancellationToken);
        }

        /// <summary>
        /// Send a GET request to the specified Uri and return the response body as a dynamic JSON.
        /// </summary>
        /// <param name="uri">The Uri the request is sent to.</param>
        /// <param name="options">Options to control the behavior during reading.</param>
        /// <param name="cancellationToken">A token that may be used to cansel the read operation.</param>
        /// <returns></returns>
        public static async Task<dynamic> GetAsync(
            Uri uri,
            JsonSerializerOptions options = null,
            CancellationToken cancellationToken = default)
        {
            if (uri == null)
                throw new ArgumentNullException(nameof(uri));

            using (var client = new HttpClient())
            using (var stream = await client.GetStreamAsync(uri))
            {
                return await ParseAsync(stream, options, cancellationToken);
            }
        }
        #endregion

    }
}
